------------------------------------------------------------
-- GearManagerEx.lua
--
-- Abin
-- 2009-6-10
------------------------------------------------------------

local L = GEARMANAGEREX_LOCALE; -- Locale table
local VERSION = GetAddOnMetadata("GearManagerEx", "Version") or "1.0";
local BANKS = { -1, 5, 6, 7, 8, 9, 10, 11 }; -- Bag id's for bank
local BAGS = { 0, 1, 2, 3, 4 }; -- Bag id's for container

local db = { talentBind = {}, stripped = {} }; -- Mapped to GearManagerExDB
local activeSet, bankOpened, rClickedSet, rClickedSetIcon, prevDropDown, inputDialog;

--Prints a message
local function PrintMessage(msg, ...)
	DEFAULT_CHAT_FRAME:AddMessage("|cffffff78GearManagerEx:|r "..tostring(msg), ...);
end

-- Create the input dialog(on-demand)
local function CreateInputDialog(name, parent)

	local frame = CreateFrame("Frame", name, parent);
	frame:Hide();
	frame:SetWidth(280);
	frame:SetHeight(120);
	frame:SetPoint("CENTER");
	frame:SetBackdrop({ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", tile = true, tileSize = 32, edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", edgeSize = 32, insets = {left = 11, right = 12, top = 12, bottom = 11 }, });
	frame:SetToplevel(true);
	frame:EnableMouse(true);
	frame:SetMovable(true);
	frame:SetFrameStrata("DIALOG");
	frame:CreateTitleRegion():SetAllPoints(frame);

	frame:SetScript("OnHide", frame.Hide);

	frame.label = frame:CreateFontString(name.."Label", "ARTWORK", "GameFontHighlightLeft");
	frame.label:SetPoint("TOPLEFT", 24, -20);

	frame.edit = CreateFrame("EditBox", name.."Edit", frame);
	frame.edit:SetWidth(144);
	frame.edit:SetHeight(18);
	frame.edit:SetTextInsets(3, 3, 2, 2);
	frame.edit:SetFontObject("GameFontHighlightLeft");
	frame.edit:SetBackdrop({ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", tile = true, tileSize = 16, edgeFile = "", edgeSize = 0, insets = {left = 0, right = 0, top = 0, bottom = 0 }, });
	frame.edit:SetPoint("LEFT", 26, 4);
	frame.edit:SetPoint("RIGHT", -26, 4);

	local edge = CreateFrame("Frame", name.."EditEdge", frame.edit);
	edge:SetBackdrop({ bgFile = "", tile = true, tileSize = 16, edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 16, insets = {left = 5, right = 5, top = 5, bottom = 5 }, });
	edge:SetBackdropBorderColor(0.75, 0.75, 0.75, 0.75);
	edge:SetPoint("TOPLEFT", -4, 4);
	edge:SetPoint("BOTTOMRIGHT", 4, -4);
	edge:SetScale(0.75);	

	frame.edit:SetScript("OnEnterPressed", function(self) self:GetParent().ok:Click() end);
	frame.edit:SetScript("OnEscapePressed", function(self) self:GetParent().cancel:Click() end);
	frame.edit:SetScript("OnEditFocusGained", function(self) self:HighlightText(); end);

	frame.ok = CreateFrame("Button", name.."OK", frame, "UIPanelButtonTemplate");
	frame.ok:SetWidth(85);
	frame.ok:SetHeight(22);
	frame.ok:SetText(OKAY);
	frame.ok:SetPoint("BOTTOMRIGHT", frame, "BOTTOM", -12, 20);
	frame.ok:SetScript("OnClick", function(self)
		local parent = self:GetParent();
		if type(parent.OnOK) == "function" and parent:OnOK(parent.edit:GetText()) then
			parent.edit:HighlightText();
		else
			parent:Hide();
		end		
	end);

	frame.cancel = CreateFrame("Button", name.."Cancel", frame, "UIPanelButtonTemplate");
	frame.cancel:SetWidth(85);
	frame.cancel:SetHeight(22);
	frame.cancel:SetText(CANCEL);
	frame.cancel:SetPoint("BOTTOMLEFT", frame, "BOTTOM", 12, 20);
	frame.cancel:SetScript("OnClick", function(self) self:GetParent():Hide() end);

	return frame;
end

-- Extracts item id
local function GetItemId(bag, slot)
	local lnk;
	if slot then
		lnk = GetContainerItemLink(bag, slot);
	else
		lnk = GetInventoryItemLink("player", bag);
	end

	if lnk then
		local _, _, id = string.find(lnk, "item:(%d+).+%[(.+)%]");
		return tonumber(id);
	end
end

-- Moves a set into bank, or take it out of bank
local function MoveSet(self, toBank)
	if not bankOpened then return end
	local containers = toBank and BAGS or BANKS;
	local id, bag, slot;
	local setids = GetEquipmentSetItemIDs(rClickedSet);
	for _, id in ipairs(setids) do
		for _, bag in ipairs(containers) do
			for slot = 1, GetContainerNumSlots(bag) do
				local itemId = GetItemId(bag, slot);
				if itemId and itemId == id then
					-- You won't be in combat while visiting a bank, will you? UseContainerItem is quite safe here...
					UseContainerItem(bag, slot);
				end
			end
		end
	end
end

-- Update checked/unchecked status for all set buttons
local function UpdateSetButtons()
	local i;
	for i = 1, MAX_EQUIPMENT_SETS_PER_PLAYER do
		local button = getglobal("GearSetButton"..i);
		if button then
			button:SetChecked(button.name and button.name ~= "" and button.name == activeSet);
			if button.name and button.name ~= "" then
				button.hotkey:Show();
			else
				button.hotkey:Hide();
			end
		end
	end
end

-- Texture to icon index
local function GetIconIDFromTexture(iconTexture)
	RefreshEquipmentSetIconInfo();
	local numIcons = GetNumMacroIcons();
	local i;
	for i = INVSLOT_FIRST_EQUIPPED, INVSLOT_LAST_EQUIPPED do
		if GetInventoryItemTexture("player", i) then
			numIcons = numIcons + 1;
		end
	end

	for i = 1, numIcons do
		local texture, id = GetEquipmentSetIconInfo(i);
		if texture == iconTexture then 
			return id;
		end
	end

	return 1;
end

-- Bind a set to a talent group
local function BindToTalent(id)
	local set = (db.talentBind[id] ~= rClickedSet) and rClickedSet or nil;
	if set then
		local i;
		for i = 1, 2 do
			if db.talentBind[i] == set then
				db.talentBind[i] = nil;
			end
		end
	end
	db.talentBind[id] = set;
end

-- Check whether a set is currently worn
local function CheckSetWorn(equipped, name)
	if not name then
		return nil;
	end

	local ids = GetEquipmentSetItemIDs(name);
	local k, v;
	for k, v in ipairs(ids) do
		if equipped[k] ~= v then
			return nil;
		end
	end
	return 1;
end

-- Find the currently worn set and update the UI
local function UpdateWornSet()
	local _, name = GearManagerEx_GetActiveSet();
	if name ~= activeSet then
		GearManagerEx_OnActiveSetChanged(name, activeSet);
	end
end

-- Directly save a set
local function SaveSet()
	if rClickedSet then		
		PrintMessage(string.format(L["set saved"], rClickedSet));
		SaveEquipmentSet(rClickedSet, GetIconIDFromTexture(rClickedSetIcon));
	end
end

local function RenameSet()
	if not inputDialog then
		inputDialog = CreateInputDialog("GearManagerExInputDialog", GearManagerDialog);		
		inputDialog.OnOK = function(self, name)
			name = string.trim(name or "");
			if name == "" then
				return 1;
			end

			if GetEquipmentSetInfoByName(name) then
				PrintMessage(string.format(L["name exists"], name), 1, 0, 0);
				return 1;
			end
			
			PrintMessage(string.format(L["set renamed"], self.setName, name));
			RenameEquipmentSet(self.setName, name);
		end
	end

	inputDialog.label:SetText(string.format(L["label text"], rClickedSet));
	inputDialog.setName = rClickedSet;
	inputDialog:Show();
end

local function DeleteSet()
	local frame = StaticPopup_Show('CONFIRM_DELETE_EQUIPMENT_SET', rClickedSet);
	frame.data = rClickedSet;
end

-- The background message handler frame
local manager = CreateFrame("Frame");
manager:Hide();
manager:RegisterEvent("VARIABLES_LOADED");
manager:RegisterEvent("PLAYER_ENTERING_WORLD");
manager:RegisterEvent("UPDATE_BINDINGS");
manager:RegisterEvent("BANKFRAME_OPENED");
manager:RegisterEvent("BANKFRAME_CLOSED");
manager:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED");
manager:RegisterEvent("PLAYER_EQUIPMENT_CHANGED");
manager:RegisterEvent("EQUIPMENT_SETS_CHANGED");

manager:SetScript("OnEvent", function(self, event, id)
	if event == "VARIABLES_LOADED" then
		SetCVar("equipmentManager", 1);
		GearManagerToggleButton:Show();

		if type(GearManagerExDB) ~= "table" then
			GearManagerExDB = {};
		end

		db = GearManagerExDB;

		if type(db.talentBind) ~= "table" then
			db.talentBind = {};
		end

		if type(db.stripped) ~= "table" then
			db.stripped = {};
		end
		
		DEFAULT_CHAT_FRAME:AddMessage("|cffffff78GearManagerEx "..VERSION.."|r by Abin loaded.");

	elseif event == "UPDATE_BINDINGS" then
		local i;
		for i = 1, MAX_EQUIPMENT_SETS_PER_PLAYER do
			local button = getglobal("GearSetButton"..i);
			if button and button.hotkey then
				local key = GetBindingKey("GEARMANAGEREX_WEARSET"..i);
				button.hotkey:SetText(GetBindingText(key, "KEY_", 1));
			end
		end
	elseif event == "BANKFRAME_OPENED" then
		bankOpened = 1;
		CloseDropDownMenus();
	elseif event == "BANKFRAME_CLOSED" then
		bankOpened = nil;
		CloseDropDownMenus();
	elseif event == "ACTIVE_TALENT_GROUP_CHANGED" then
		local set = db.talentBind[id];
		if set then
			EquipmentManager_EquipSet(set);
		end
	elseif event == "PLAYER_ENTERING_WORLD" then
		self:UnregisterEvent("PLAYER_ENTERING_WORLD");
		UpdateWornSet();
		self:Show();
	elseif event == "PLAYER_EQUIPMENT_CHANGED" then
		if self:IsShown() then
			self.checkTime = GetTime() + 0.5; -- Highly concurrent events, use delay-processing to avoid performance drop...
		end
	elseif event == "EQUIPMENT_SETS_CHANGED" then
		UpdateWornSet();
	end
end);

manager:SetScript("OnUpdate", function(self, elapsed)
	if self.checkTime and GetTime() > self.checkTime then
		self.checkTime = nil;
		UpdateWornSet();
	end
end);

-- The drop down setMenu for set buttons
local setMenu = CreateFrame("Button", "GearManagerExDropDownButtonSetMenu", manager, "UIDropDownMenuTemplate");
setMenu.point = "TOPLEFT";
setMenu.relativePoint = "BOTTOMLEFT";
setMenu.xOffset = -4;
setMenu.yOffset = -4;

-- Our menu items for advanced set operations
UIDropDownMenu_Initialize(setMenu, function()
	UIDropDownMenu_AddButton({ text = rClickedSet, isTitle = 1, icon = rClickedSetIcon });
	UIDropDownMenu_AddButton({ text = L["save set"], disabled = activeSet, func = SaveSet });
	UIDropDownMenu_AddButton({ text = L["delete set"], func = DeleteSet });
	UIDropDownMenu_AddButton({ text = L["rename set"], func = RenameSet });
	UIDropDownMenu_AddButton({ text = L["put into bank"], disabled = not bankOpened, func = MoveSet, arg1 = 1 });
	UIDropDownMenu_AddButton({ text = L["take from bank"], disabled = not bankOpened, func = MoveSet });
	if GetNumTalentGroups() > 1 then
		UIDropDownMenu_AddButton({ text = L["bind to"]..TALENT_SPEC_PRIMARY, checked = db.talentBind[1] == rClickedSet, func = function() BindToTalent(1) end });
		UIDropDownMenu_AddButton({ text = L["bind to"]..TALENT_SPEC_SECONDARY, checked = db.talentBind[2] == rClickedSet, func = function() BindToTalent(2) end });
	end
	UIDropDownMenu_AddButton({ text = CLOSE });
end, "MENU");

--------------------------------------------------------------
-- Create Hotkey text for GearSetButton1-10
--------------------------------------------------------------
local i;
for i = 1, MAX_EQUIPMENT_SETS_PER_PLAYER do
	local button = getglobal("GearSetButton"..i);
	if button then
		button.hotkey = button:CreateFontString(button:GetName().."HotKey", "OVERLAY", "NumberFontNormalSmallGray");
		button.hotkey:SetJustifyH("RIGHT");
		button.hotkey:SetPoint("TOPLEFT", 2, -2);
		button.hotkey:SetPoint("TOPRIGHT", -2, -2);
		
		-- Hook "OnMouseDown" for every button to show the drop down menu
		button:HookScript("OnMouseDown", function(self, button)
			if prevDropDown ~= self then
				CloseDropDownMenus();
			end

			if button == "RightButton" and self.name and self.name ~= "" then
				prevDropDown = self;
				setMenu.relativeTo = self;
				rClickedSet = self.name;
				rClickedSetIcon = self.icon:GetTexture();
				ToggleDropDownMenu(nil, nil, setMenu);
			end
		end);		
	end
end

-- Colorize set tooltip texts with item quality, instead of plain white
local function ReplaceTooltipTextColor(fs)
	if fs and fs:GetText() then
		local _, _, quality = GetItemInfo(fs:GetText());
		if quality then
			local r, g, b = GetItemQualityColor(quality);
			fs:SetTextColor(r, g, b);
		end
	end
end

local function HookGameTooltipContents()
	local i;
	for i = 1, GameTooltip:NumLines() do
		ReplaceTooltipTextColor(getglobal("GameTooltipTextLeft"..i));
		ReplaceTooltipTextColor(getglobal("GameTooltipTextRight"..i));
	end
end

-- Hook GameTooltip if it's displaying an equipment set
hooksecurefunc(GameTooltip, "SetEquipmentSet", HookGameTooltipContents);
hooksecurefunc(GameTooltip, "SetAction", function(self, action)
	if GetActionInfo(action) == "equipmentset" then
		HookGameTooltipContents();
	end
end);

-- Prompt the player that he can right-click a set for more operations
hooksecurefunc("GearSetButton_OnEnter", function(self)
	if self.name and self.name ~= "" and GameTooltipTextLeft1:GetText() == self.name then		
		GameTooltip:AddLine(" ");
		GameTooltip:AddDoubleLine(L["set hotkey"], self.hotkey:GetText(), nil, nil, nil, 1, 1, 1);		
		GameTooltip:AddLine(L["tooltip prompt"], 0, 1, 0, 1);
		GameTooltip:Show();
	end
end);

-- Update the checked/unchecked stats for every set button
hooksecurefunc("GearManagerDialog_Update", function()
	UpdateSetButtons();
end);

-- Need to hide our menu when UI is closed
GearManagerDialog:HookScript("OnHide", function(self)
	CloseDropDownMenus();
end);

--------------------------------------------------------------
-- Quick strip
--------------------------------------------------------------

local DURASLOTS = { 16, 17, 18, 5, 7, 1, 3, 8, 10, 6, 9 }; -- Inventory slots that possibly have durabilities
local lastOpt = 0; -- Last operated time

-- Find available bag slots to put items into
local function GetAvailableBags()
	local bags = {};
	local i;
	for i = 0, 4 do		
		local slots, family = GetContainerNumFreeSlots(i);
		bags[i] = (family == 0) and slots or 0;
	end
	return bags;
end

local function GetFirstAvailableBag(bags)
	local i;
	for i = 0, 4 do
		if bags[i] > 0 then
			return i;
		end
	end
end

local function FindContainerItem(lnk)
	if type(lnk) ~= "string" then
		return;
	end

	local bag, slot;
	for bag = 0, 4 do
		for slot = 1, GetContainerNumSlots(bag) do
			if GetContainerItemLink(bag, slot) == lnk then
				return bag, slot;
			end
		end
	end
	return nil, nil;
end

-- Strip off all items those have durabilities
local function StripOff()
	local bags = GetAvailableBags();
	local count = 0;
	local i;
	for i = 1, table.getn(DURASLOTS) do
		local slot = DURASLOTS[i];
		if GetInventoryItemDurability(slot) and (slot > 15 or not InCombatLockdown()) then
			local lnk = GetInventoryItemLink("player", slot);
			if lnk then
				local bag = GetFirstAvailableBag(bags);
				if bag then
					bags[bag] = bags[bag] - 1;
					PickupInventoryItem(slot);
					if CursorHasItem() then					
						if bag == 0 then
							PutItemInBackpack();
						else
							PutItemInBag(19 + bag);
						end					

						table.insert(db.stripped, { ["link"] = lnk, ["slot"] = slot});
						count = count + 1;
					end
				else
					PrintMessage(ERR_INV_FULL, 1, 0, 0);
					return count;
				end
			end
		end
	end
	return count;
end

-- Wear back whatever we stripped off through a previous call to StripOff()
local function WearBack()
	local count = 0;
	local i;
	for i = table.getn(db.stripped), 1, -1 do
		local data = db.stripped[i];
		if type(data.link) ~= "string" or type(data.slot) ~= "number" then
			table.remove(db.stripped, i);
		elseif data.slot > 15 or not InCombatLockdown() then
			local bag, slot = FindContainerItem(data.link);
			if bag and slot then
				PickupContainerItem(bag, slot);
				if CursorHasItem() then
					EquipCursorItem(data.slot);
					count = count + 1;
				end
			end
			table.remove(db.stripped, i);
		end
	end
	return count;
end

------------------------------------------------------------
-- Globals
------------------------------------------------------------

-- The purpose of making this function global is to give other addons a chance to get notified when the "active set"
-- changes, it is called when either a set is completely worn, or a previously worn set becomes no longer complete. 
-- "currentName" - the current active set name, can be nil.
-- "previousName" - the previous active set name, can be nil.
-- For example, if "currentName" is nil and "previousName" is not, it indicates that some piece(s) of the set has
-- just been taken off.
-- Other addons may catch this event by secure-hooking "GearManagerEx_OnActiveSetChanged".
function GearManagerEx_OnActiveSetChanged(currentName, previousName)
	CloseDropDownMenus();
	activeSet = currentName;
	UpdateSetButtons();
	if currentName then
		PrintMessage(string.format(L["wore set"], currentName));
	end
end

-- Get the current active set info: index, name, icon
function GearManagerEx_GetActiveSet()
	local equipped = {};
	local i;
	for i = 0, 19 do
		equipped[i] = GetItemId(i) or 0;
	end

	for i = 1, GetNumEquipmentSets() do
		local name, icon, internalId = GetEquipmentSetInfo(i);
		if CheckSetWorn(equipped, name) then
			return i, name, icon, internalId;
		end
	end
end

-- Show/hide GearManagerDialog
function GearManagerEx_ToggleUI()
	if GearManagerDialog:IsVisible() then
		GearManagerDialog:Hide();
	else
		if not PaperDollFrame:IsVisible() then
			ToggleCharacter("PaperDollFrame");
		end
		GearManagerDialog:Show();
	end
end

-- Strip/wear
function GearManagerEx_QuickStrip()
	if UnitIsDeadOrGhost("player") or not HasFullControl() or UnitOnTaxi("player") or UnitCastingInfo("player") then
		PrintMessage(ERR_CLIENT_LOCKED_OUT, 1, 0, 0);
		return;
	end

	if GetTime() - lastOpt < 2 then
		PrintMessage(L["too fast"]);
		return;
	end

	if table.getn(db.stripped) > 0 then
		PrintMessage(string.format(L["wore back"], WearBack()));

	else
		PrintMessage(string.format(L["stripped off"], StripOff()));
	end
	lastOpt = GetTime();
end

-- Equip a set by index
function GearManagerEx_EquipSetByIndex(id)
	if type(id) == "number" then
		local name = GetEquipmentSetInfo(id);
		if name then
			EquipmentManager_EquipSet(name);
		end
	end	
end

------------------------------------------------------------
-- Binding texts
------------------------------------------------------------

BINDING_HEADER_GEARMANAGEREX_TITLE = "GearManagerEx";
BINDING_NAME_GEARMANAGEREX_TOGGLEUI = L["open"]..EQUIPMENT_MANAGER;
BINDING_NAME_GEARMANAGEREX_QUICKSTRIP = L["quick strip"];
BINDING_NAME_GEARMANAGEREX_WEARSET1 = L["wear set"].." 1";
BINDING_NAME_GEARMANAGEREX_WEARSET2 = L["wear set"].." 2";
BINDING_NAME_GEARMANAGEREX_WEARSET3 = L["wear set"].." 3";
BINDING_NAME_GEARMANAGEREX_WEARSET4 = L["wear set"].." 4";
BINDING_NAME_GEARMANAGEREX_WEARSET5 = L["wear set"].." 5";
BINDING_NAME_GEARMANAGEREX_WEARSET6 = L["wear set"].." 6";
BINDING_NAME_GEARMANAGEREX_WEARSET7 = L["wear set"].." 7";
BINDING_NAME_GEARMANAGEREX_WEARSET8 = L["wear set"].." 8";
BINDING_NAME_GEARMANAGEREX_WEARSET9 = L["wear set"].." 9";
BINDING_NAME_GEARMANAGEREX_WEARSET10 = L["wear set"].." 10";